jQuery(function() {

/*
 * Overriding Array prototype (for convenience)
 * 
 */
if (typeof Array.prototype.map == 'undefined') {
  Array.prototype.map = function(iterator, thisObj) {
    for (var i=0,len=this.length,ret=[];i<len;i++) {
      ret.push(iterator.call(thisObj, this[i], i, this));
    }
    return ret;
  }
}

if (typeof Array.prototype.forEach == 'undefined') {
  Array.prototype.forEach = function(iterator, thisObj) {
    for (var i=0,len=this.length;i<len;i++) {
      iterator.call(thisObj, this[i], i, this)
    }
  }
}

if (typeof Array.prototype.filter == 'undefined') {
  Array.prototype.filter = function(iterator, thisObj) {
    for (var i=0,len=this.length,ret=[];i<len;i++) {
      if (iterator.call(thisObj, this[i], i, this)) ret.push(this[i]);
    }
    return ret;
  }
}

Array.prototype.find = function(iterator, thisObj) {
  for (var i=0,len=this.length;i<len;i++) {
    if (iterator.call(thisObj, this[i], i, this)) return this[i];
  }
  return undefined;
}

Array.prototype.inject = function(memo, iterator) {
  this.forEach(function(value, index) {
    memo = iterator(memo, value, index);
  });
  return memo;
}

Array.prototype.flatten = function() {
  return this.inject([], function(array, value) {
    return array.concat(value && value.constructor == Array ?
                        value.flatten() : 
                        [value]);
  })
}

/*
 * Import other org data, exported from salesforce.com (via DataLoader) This
 * class automatically resolves inter-record dependencies.
 */
var SerializedUploader = function() {
  this.init();
}

SerializedUploader.prototype = {

  init : function() {
    this.sobject = {};

    this.idmap = {};
    this.target = {};

    this.successes = [];
    this.failures = [];

    this.listeners = [];
  }
  ,

  fireEvent : function(event) {
    this.listeners.forEach(function(lis) { lis['on'+event] && lis['on'+event]() });
  }
  ,

  addListener : function(listener) {
    this.listeners.push(listener);
  }
  ,

  describe : function(table) {
    if (this.sobject[table] && this.sobject[table].described) {
      return this.sobject[table].described;
    } else {
      this.sobject[table] = this.sobject[table] || {};
      return (this.sobject[table].described = sforce.connection.describeSObject(table));
    }
  }
  ,

  getFieldDef : function(table, fname) {
    return this.describe(table).fields.find(function(field) { 
      return field.name.toUpperCase() == fname.toUpperCase() 
    });
  }
  ,

  getType : function(table, fname) {
    var f = this.getFieldDef(table, fname);
    return f ? f.type : null;
  }
  ,

  setUploadingTarget : function(ids) {
    var _this = this;
    ids.forEach(function(id) { _this.target[id] = true });
  }
  ,

  isTargetedUpload : function() {
    for (var id in this.target) {
      return true;
    }
    return false;
  }
  ,

  upload : function() {
    var _this = this;
    var uploadings = []; // array of sobj and recordId (old) pair

    for (table in this.sobject) {
      var records = this.filterUploadableRecords(table)
      uploadings = uploadings.concat(
                     records.map(function(record) {
                       return _this.convertToSObjectPair(table, record) 
                     })
                   );
    }

    if (uploadings.length > 0) {
      this.uploadSObjects(uploadings, function() {
        // event notification;
        _this.fireEvent('UploadProgress');
        // recursive call
        _this.upload();
      });
    } else {
      this.fireEvent('Complete');
    }
  }
  ,

  filterUploadableRecords : function(table) {
    var _this = this;

    var records = this.sobject[table].records;
    var header = this.sobject[table].header;

    // search id and reference id column index
    var idIndex;
    var ridIndexes = [];
    header.forEach(function(h, i) {
      var type = _this.getType(table, h);
      if (type=='id') {
        idindex = i;
      } else if (type=='reference') {
        ridIndexes.push(i);
      }
    });

    var uploadables = [];
    var waitings = [];

    var targeted = this.isTargetedUpload();

    records.forEach(function(record) {
      var id = record[idindex];
      var upOK = !targeted || _this.target[id];

      ridIndexes.forEach(function(i) {
        var rid = record[i];
        if (rid.length>0) {
          if (targeted) { 
            if (_this.target[rid]) { // if parent record is in targets
              _this.target[id] = true; // child record should be in targets,
										// also.
            } else if (_this.target[id]) { // if child record is in targets
              _this.target[rid] = true; // parent record should be in targets,
										// also.
            }
          }
          if (!_this.idmap[rid]) { // if parent record not uploaded
            upOK = false;
          }
        }
      })

      if (upOK) uploadables.push(record);
      else waitings.push(record);

    })

    this.sobject[table].records = waitings;
    return uploadables;

  }
  ,

  convertToSObjectPair : function(table, record) {
    var _this = this;

    var sobj = new sforce.SObject(table);
    var id;

    record.forEach(function(value, i) {
      var fieldDef = _this.getFieldDef(table, _this.sobject[table].header[i]);
      if (fieldDef!=null) {
        var creatable = fieldDef.getBoolean('createable');
        switch (fieldDef.type) {
          case 'id' :
            id = value;
            break;
          case 'int' :
            var num = parseInt(value);
            if (!isNaN(num) && creatable) sobj[fieldDef.name] = num;
            break;
          case 'double' :
          case 'currency' :
          case 'percent' :
            var fnum = parseFloat(value);
            if (!isNaN(fnum) && creatable) sobj[fieldDef.name] = fnum;
            break;
          case 'date' :
          case 'datetime' :
            if (record[i].length>0 && creatable) sobj[fieldDef.name] = value;
            break;
          case 'reference' :
            if (creatable) sobj[fieldDef.name] = _this.idmap[value];
            break;
          default :
            if (creatable) sobj[fieldDef.name] = value;
            break;
        }
      }
    });

    // In Chrome browser, for-in loop doesn't preserve the order of property declaration.
    // Re-order properties so that "type" property to be listed at first.
    (function(len) {
      if (len<0) return; // to avoid inf loop;
      for (var k in sobj) { 
        if (typeof sobj[k] == 'function' || !sobj.hasOwnProperty[k]) continue;
        if (k !== 'type') {
          var v = sobj[k];
          delete sobj[k];
          sobj[k] = v;
          arguments.callee(len-1);
        }
        break;
      }
    })(record.length);

    return { id : id, sobject : sobj };

  }
  ,


  uploadSObjects : function(uploadings, next) {
    var _this = this;

    var sobjects = uploadings.map(function(r) { return r.sobject });

    bulkInsert(sobjects, function(rets) {
      rets.forEach(function (ret, i) {
        if (ret.getBoolean('success')) {
          // register map info of oldid -> newid
          _this.idmap[uploadings[i].id] = ret.id;
          _this.successes.push([uploadings[i].id, ret.id]);
        } else {
          _this.failures.push([uploadings[i].id, ret]);
        }
      });

      // continuation call
      next();
    })

    // bulk create function for over 200 records
    // update 2010/11/01 API 20.0 allows inserting multiple type records in one call.
    function bulkInsert(sobjects, callback) {
      var limit = 200;
      if (sobjects.length>limit) {
        var sobjs = sobjects.slice(0, limit)
        sforce.connection.create(sobjs, function(rets) {
          bulkInsert(sobjects.slice(limit), function(rets2) {
            callback(rets.concat(rets2));
          });
        });
      } else {
        sforce.connection.create(sobjects, callback);
      }
    }
    
  }
  ,

  loadCSV : function(table, csvtext) {
    var csvp = new CSVParser(csvtext);
    this.sobject[table] = this.sobject[table] || {};
    this.sobject[table].header = [];
    this.sobject[table].records = [];
    var token;
    while ((token = csvp.nextToken())!=null) {
      this.sobject[table].header.push(token);
    }
    while (csvp.nextLine()) {
      var record = [];
      while ((token = csvp.nextToken())!=null) {
        record.push(token);
      }
      this.sobject[table].records.push(record);
    }
    
    // pre-fetch describe data
    this.describe(table);
  }
}

/**
 * Very, very simple CSV Parser
 */
var CSVParser = function(text) {
  this.text = text;
  this.cursor = 0;
}

CSVParser.prototype = {
  nextToken : function() {
    var cell;
    var dquoted = false;
    var firstChar = this.text.charAt(this.cursor);
    if (firstChar == '' || firstChar == '\n' || firstChar == '\r') return null;
    if (firstChar == '"') dquoted = true;
    if (dquoted) {
      var dq = this.cursor;
      while(true) {
        dq++;
        dq = this.text.indexOf('"', dq);
        if (dq<0 || this.text.charAt(dq+1)!='"') break;
        else dq++;
      }
      if (dq>=0) {
        var delim = this.text.charAt(dq+1);
        cell = this.text.substring(this.cursor, dq+1);
        this.cursor = dq + (delim == ',' ? 2 : 1);
      } else {
        cell = this.text.substring(this.cursor);
        this.cursor = this.text.length;
      }
      return cell.replace(/""/g,'"').replace(/^"/,'').replace(/"$/,'');
    } else {
      var comma = this.text.indexOf(',', this.cursor);
      var ret = this.text.indexOf('\n', this.cursor);
      var pivot = comma > 0 && ret > 0 ? (comma < ret ? comma : ret) :
                  comma > 0 ? comma : 
                  ret > 0 ? ret : this.text.length;
      cell = this.text.substring(this.cursor, pivot)
      this.cursor = pivot;
      if (comma==pivot) this.cursor++;
      return cell;
    }
  }
  ,

  nextLine : function() {
    for(var c = this.text.charAt(this.cursor);
        c == '\n' || c == '\r';
        c = this.text.charAt(++this.cursor))
      ;
    if (this.cursor == this.text.length) return false
    else return true;
  }
}


/**
 * Application code from here
 */

var RESOURCES = (function() {
  function getResource(resName) {
    return $('.du-resources .'+resName.toLowerCase().replace(/_/g, '-')).val();
  }
  var res = {
    MSG_IMPORT_CONFIRMATION : 1,
    MSG_RETRIEVING_DATA_FILES : 1,
    MSG_IMPORT_PROGRESS : 1,
    MSG_ADJUSTING_TASK : 1,
    MSG_ADJUSTING_EVENT : 1,
    MSG_ADJUSTING_OPP : 1,
    MSG_COMPLETE_IMPORT : 1,
    MSG_TRUNCATE_CONFIRMATION : 1,
    MSG_COMPLETE_TRUNCATE : 1,
    FOLDER_CSV_DATA : 1
  }
  for (var resName in res) {
    res[resName] = getResource(resName);
  }
  return res;
})();

var importProgress = 0;

var su = new SerializedUploader();

function importData() {
  clearMessage();
  if (confirm(RESOURCES.MSG_IMPORT_CONFIRMATION)) {
    try {
      downloadCSVDocs();
    } catch(e) {
      alert(e);
    }
  }
}

function downloadCSVDocs() {
  importProgress = 0;
  showMessage(RESOURCES.MSG_RETRIEVING_DATA_FILES);
  var docs = sforce.connection.query(
    "SELECT Id, Name FROM Document "+
    "WHERE Folder.DeveloperName = 'ImportingCSVDataFolder' "+
    "AND Name LIKE '%.csv'"
  ).getArray('records');

  (function() {
     var call = arguments.callee;
     var doc = docs.pop();
     if (doc) {
       var table = doc.Name.replace(/\.csv$/, '');
       downloadCSV(doc.Id, function(csvtext) {
         su.loadCSV(table, csvtext);
         call();
       })
     } else {
       uploadData();
     }
  })();
}

function downloadCSV(docId, callback) {
  importProgress += 2;
  showMessage(RESOURCES.MSG_RETRIEVING_DATA_FILES);
  sforce.connection.query(
    "SELECT Id, Body FROM Document WHERE Id = '"+docId+"' LIMIT 1", function(res) {
    var body = res.getArray('records')[0].Body;
    var csvtext = toUTF8Str(decodeBase64(body));
    callback(csvtext);
  });
}

var decodeBase64 = (function() {

  var base64DecodeChars = [
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 ];

  return function(str) {
    var len = str.length;
    var i = 0;
    var out = [];

    while(i < len) {
      var c1, c2, c3, c4;

      /* c1 */
      do {
        c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
      } while (i < len && c1 == -1);
      if (c1 == -1) break;

      /* c2 */
      do {
        c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
      } while(i < len && c2 == -1);
      if(c2 == -1) break;

      out.push((c1 << 2) | ((c2 & 0x30) >> 4));

      /* c3 */
      do {
        c3 = str.charCodeAt(i++) & 0xff;
        if (c3 == 61) return out;
        c3 = base64DecodeChars[c3];
      } while(i < len && c3 == -1);
      if(c3 == -1) break;

      out.push(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));

      /* c4 */
      do {
        c4 = str.charCodeAt(i++) & 0xff;
        if (c4 == 61) return out;
        c4 = base64DecodeChars[c4];
      } while(i < len && c4 == -1);
      if(c4 == -1) break;
      out.push(((c3 & 0x03) << 6) | c4);
    }
    return out;
  }

})();


function toUTF8Str(utf8) {
  var str = "";
  var len = utf8.length;
  var i = 0;
  while (i <len) {
    c = utf8[i++];
    switch(c >> 4) { 
      case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
        // 0xxxxxxx
        str += String.fromCharCode(utf8[i-1]);
	    break;
	  case 12: case 13:
	    // 110x xxxx   10xx xxxx
	    var c2 = utf8[i++];
	    str += String.fromCharCode(((c & 0x1F) << 6) | (c2 & 0x3F));
	    break;
	  case 14:
	    // 1110 xxxx  10xx xxxx  10xx xxxx
	    var c2 = utf8[i++];
	    var c3 = utf8[i++];
	    str += String.fromCharCode(((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | ((c3 & 0x3F) << 0));
	    break;
    }
  }
  return str;
}


function uploadData() {
  su.addListener({
    onUploadProgress : function() {
      importProgress += 10;
      showMessage(RESOURCES.MSG_IMPORT_PROGRESS.replace('{0}', su.successes.length));
    }
    ,
    onComplete : function() {
      if (typeof adjustDateValue != 'undefined' && adjustDateValue) {
        adjustData();
      } else {
        importComplete();
      }
    }
  });
  su.upload();
}


/**
 * Update the date field value to current date.
 * It's very adhoc procedure, depends on CSV data.
 */
function adjustData() {
  importProgress = 60;
  adjustTask(function() {
    adjustEvent(function() {
      adjustOpportunity(importComplete);
    })
  })
}

function adjustTask(next) {
  importProgress += 10;
  showMessage(RESOURCES.MSG_ADJUSTING_TASK);
  
  var pivot = 1176390000000; // 2007-04-13T00:00:00+0900
  var today = new Date();
  today.setHours(0);
  today.setMinutes(0);
  today.setSeconds(0);
  today.setMilliseconds(0);
  var td = today.getTime();

  adjustDateFields('Task', [ 'ActivityDate' ], pivot, td, next);
}

function adjustEvent(next) {
  importProgress += 10;
  showMessage(RESOURCES.MSG_ADJUSTING_EVENT);

  var pivot = 1177254000000; // 2007-04-23T00:00:00+0900
  var md = new Date();
  md.setHours(0);
  md.setMinutes(0);
  md.setSeconds(0);
  md.setMilliseconds(0);
  md.setDate(md.getDate()-md.getDay()+1);
  md = md.getTime();

  adjustDateFields('Event', [ 'ActivityDateTime' ], pivot, md, next);
}


function adjustOpportunity(next) {
  importProgress += 10;
  showMessage(RESOURCES.MSG_ADJUSTING_OPP);
  
  var pivot = 1177254000000; // 2007-04-23T00:00:00+0900
  var md = new Date();
  md.setHours(0);
  md.setMinutes(0);
  md.setSeconds(0);
  md.setMilliseconds(0);
  md.setDate(md.getDate()-md.getDay()+1);
  md = md.getTime();
  
  adjustDateFields('Opportunity', [ 'CloseDate' ], pivot, md, next);
}


function adjustDateFields(table, dtfields, pivot, now, next) {

  var ids = su.successes.map(function(id){ return id[1] })
                        .filter(function(id) {
                           return id.match(new RegExp('^'+su.describe(table).keyPrefix))
                         });
  sforce.connection.update(
	  sforce.connection.retrieve(
	    "Id,"+dtfields.join(','), table, ids
	  ).map(function(r) {
	    dtfields.forEach(function(field) {
	      var d = su.getFieldDef(table,field).type=='datetime' ? 
	              r.getDateTime(field) : 
	              r.getDate(field);
	      d = d ? d.getTime() : null;
	      if (d) r[field] = new Date(now + (d-pivot));
	    })
	    return r;
	  })
	  ,
	  next 
	)
}


function importComplete() {
   clearMessage();
   alert(RESOURCES.MSG_COMPLETE_IMPORT);
}


$(function() {
  $('.msgDialog').dialog({
    width : 600,
    modal : true,
    closeOnEscape : false,
    autoOpen : false,
    resizable : false,
    draggable : false,
    open: function(event, ui) { $(".ui-dialog-titlebar-close").hide(); }
  });
  $('.msgDialog .msgProgressBar').progressbar({ value : 0 });
})

function showMessage(message, value) {
  $('.msgDialog').dialog('open');
  $('.msgDialog .msgText').text(message);
  $('.msgDialog .msgProgressBar').progressbar("value", importProgress);
}

function clearMessage() {
  $('.msgDialog').dialog('close');
  $('.msgDialog .msgText').text('');
  $('.msgDialog .msgProgressBar').progressbar("value", 0);
}


/*
 * This method will not be invoked from official interface, for development
 * usage only.
 */
function truncateAllTables() {
  var tables = sforce.connection.query(
    "SELECT Id, Name FROM Document "+
    "WHERE Folder.DeveloperName = 'ImportingCSVDataFolder' "+
    "AND Name LIKE '%.csv'"
  ).getArray('records').map(function(r){
    return r.Name.replace(/\.csv$/,'');
  });

  if (confirm(RESOURCES.MSG_TRUNCATE_CONFIRMATION.replace('{0}', tables.join(', ')))) {
    var rets = tables.map(function(table) {
      return (function(ids) {
        return (ids.length>200 ? arguments.callee(ids.slice(200)) : []).concat(
                  sforce.connection.deleteIds(ids.slice(0,200))
               );
      })
      (
	    	(function(qr) {
	        return qr.getArray('records').concat(
	                 qr.getBoolean('done') ? [] : 
	                 arguments.callee(sforce.connection.queryMore(qr.queryLocator))
	               );
	      })
	      (sforce.connection.query("SELECT Id FROM "+table))
	      .map(function(r) { return r.Id })
		  );
    }).flatten();
    var snum = rets.filter(function(r){return r.getBoolean('success')}).length;
    alert(RESOURCES.MSG_COMPLETE_TRUNCATE.replace('{0}', snum).replace('{1}', rets.length-snum));
  }
}


/**
 * Methods open to public 
 */
window.DataUploadingApp = {
  importData : importData,
  truncateAllTables : truncateAllTables
};


})
